/**
 * 
 */
package com.google.code.cisx.converters;

import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.DynaClass;
import org.apache.commons.beanutils.DynaProperty;
import org.apache.commons.beanutils.MappedPropertyDescriptor;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.beanutils.expression.Resolver;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.beans.IndexedPropertyDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author <a href='mailto:rahul.thakur.xdev@gmail.com'>Rahul Thakur</a>
 * @since 1.0
 * @version $Id: BeanUtilsBeanExtended.java 131 2008-01-20 07:48:47Z
 *          rahul.thakur.xdev $
 */
public class BeanUtilsBeanExtended extends BeanUtilsBean {

  /**
   * Commons Logging.
   */
  private static final Log log = LogFactory.getLog(BeanUtilsBeanExtended.class);

  /**
   * 
   */
  public BeanUtilsBeanExtended() {
    this(new ConvertUtilsBeanExtended(), new PropertyUtilsBean());
  }

  /**
   * @param convertUtilsBean
   * @param propertyUtilsBean
   */
  public BeanUtilsBeanExtended(ConvertUtilsBeanExtended convertUtilsBean,
      PropertyUtilsBean propertyUtilsBean) {
    super(convertUtilsBean, propertyUtilsBean);
  }

  /**
   * @param convertUtilsBean
   */
  public BeanUtilsBeanExtended(ConvertUtilsBeanExtended convertUtilsBean) {
    super(convertUtilsBean);
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.commons.beanutils.BeanUtilsBean#describe(java.lang.Object)
   */
  @Override
  public Map describe(Object bean) throws IllegalAccessException, InvocationTargetException,
      NoSuchMethodException {
    if (bean == null) {
      // return (Collections.EMPTY_MAP);
      return (new java.util.HashMap());
    }

    if (log.isDebugEnabled()) {
      log.debug("Describing bean: " + bean.getClass().getName());
    }

    Map description = new HashMap();
    if (bean instanceof DynaBean) {
      DynaProperty[] descriptors = ((DynaBean) bean).getDynaClass().getDynaProperties();
      for (int i = 0; i < descriptors.length; i++) {
        String name = descriptors[i].getName();
        description.put(name, getProperty(bean, name));
      }
    } else {
      PropertyDescriptor[] descriptors = getPropertyUtils().getPropertyDescriptors(bean);
      for (int i = 0; i < descriptors.length; i++) {
        String name = descriptors[i].getName();
        if (getPropertyUtils().getReadMethod(descriptors[i]) != null) {
          description.put(name, getPropertyAsObject(bean, name));
        }
      }
    }
    return (description);

  }

  public Object getPropertyAsObject(Object bean, String name) throws IllegalAccessException,
      InvocationTargetException, NoSuchMethodException {
    return (getNestedPropertyAsObject(bean, name));
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.commons.beanutils.BeanUtilsBean#getNestedProperty(java.lang.Object,
   *      java.lang.String)
   */
  public Object getNestedPropertyAsObject(Object bean, String name) throws IllegalAccessException,
      InvocationTargetException, NoSuchMethodException {
    // return super.getNestedProperty(bean, name);
    Object value = getPropertyUtils().getNestedProperty(bean, name);
    ConvertUtilsBean convertUtils = getConvertUtils();
    if (convertUtils instanceof ConvertUtilsBeanExtended)
      return (((ConvertUtilsBeanExtended) convertUtils).convertAsObject(value));
    else
      return (convertUtils.convert(value));

  }

  /**
   * {@inheritDoc}
   * <p>
   * We override this as we don't want an empty {@link String} value for a bean
   * property that is not of type {@link String} but of type {@link List} for
   * example.
   * 
   * @see org.apache.commons.beanutils.BeanUtilsBean#setProperty(java.lang.Object,
   *      java.lang.String, java.lang.Object)
   */
  @Override
  public void setProperty(Object bean, String name, Object value) throws IllegalAccessException,
      InvocationTargetException {
    // Trace logging (if enabled)
    if (log.isTraceEnabled()) {
      StringBuffer sb = new StringBuffer("  setProperty(");
      sb.append(bean);
      sb.append(", ");
      sb.append(name);
      sb.append(", ");
      if (value == null) {
        sb.append("<NULL>");
      } else if (value instanceof String) {
        sb.append((String) value);
      } else if (value instanceof String[]) {
        String[] values = (String[]) value;
        sb.append('[');
        for (int i = 0; i < values.length; i++) {
          if (i > 0) {
            sb.append(',');
          }
          sb.append(values[i]);
        }
        sb.append(']');
      } else {
        sb.append(value.toString());
      }
      sb.append(')');
      log.trace(sb.toString());
    }

    // Resolve any nested expression to get the actual target bean
    Object target = bean;
    Resolver resolver = getPropertyUtils().getResolver();
    while (resolver.hasNested(name)) {
      try {
        target = getPropertyUtils().getProperty(target, resolver.next(name));
        name = resolver.remove(name);
      } catch (NoSuchMethodException e) {
        return; // Skip this property setter
      }
    }
    if (log.isTraceEnabled()) {
      log.trace("    Target bean = " + target);
      log.trace("    Target name = " + name);
    }

    // Declare local variables we will require
    String propName = resolver.getProperty(name); // Simple name of target
    // property
    Class type = null; // Java type of target property
    int index = resolver.getIndex(name); // Indexed subscript value (if any)
    String key = resolver.getKey(name); // Mapped key value (if any)

    // Calculate the property type
    if (target instanceof DynaBean) {
      DynaClass dynaClass = ((DynaBean) target).getDynaClass();
      DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
      if (dynaProperty == null) {
        return; // Skip this property setter
      }
      type = dynaProperty.getType();
    } else {
      PropertyDescriptor descriptor = null;
      try {
        descriptor = getPropertyUtils().getPropertyDescriptor(target, name);
        if (descriptor == null) {
          return; // Skip this property setter
        }
      } catch (NoSuchMethodException e) {
        return; // Skip this property setter
      }
      if (descriptor instanceof MappedPropertyDescriptor) {
        if (((MappedPropertyDescriptor) descriptor).getMappedWriteMethod() == null) {
          if (log.isDebugEnabled()) {
            log.debug("Skipping read-only property");
          }
          return; // Read-only, skip this property setter
        }
        type = ((MappedPropertyDescriptor) descriptor).getMappedPropertyType();
      } else if (index >= 0 && descriptor instanceof IndexedPropertyDescriptor) {
        if (((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod() == null) {
          if (log.isDebugEnabled()) {
            log.debug("Skipping read-only property");
          }
          return; // Read-only, skip this property setter
        }
        type = ((IndexedPropertyDescriptor) descriptor).getIndexedPropertyType();
      } else if (key != null) {
        if (descriptor.getReadMethod() == null) {
          if (log.isDebugEnabled()) {
            log.debug("Skipping read-only property");
          }
          return; // Read-only, skip this property setter
        }
        type = (value == null) ? Object.class : value.getClass();
      } else {
        if (descriptor.getWriteMethod() == null) {
          if (log.isDebugEnabled()) {
            log.debug("Skipping read-only property");
          }
          return; // Read-only, skip this property setter
        }
        type = descriptor.getPropertyType();
      }
    }

    // Convert the specified value to the required type
    Object newValue = null;
    if (type.isArray() && (index < 0)) { // Scalar value into array
      if (value == null) {
        String[] values = new String[1];
        values[0] = (String) value;
        newValue = getConvertUtils().convert((String[]) values, type);
      } else if (value instanceof String) {
        newValue = getConvertUtils().convert(value, type);
      } else if (value instanceof String[]) {
        newValue = getConvertUtils().convert((String[]) value, type);
      } else {
        newValue = convert(value, type);
      }
    } else if (type.isArray()) { // Indexed value into array
      if (value instanceof String || value == null) {
        newValue = getConvertUtils().convert((String) value, type.getComponentType());
      } else if (value instanceof String[]) {
        newValue = getConvertUtils().convert(((String[]) value)[0], type.getComponentType());
      } else {
        newValue = convert(value, type.getComponentType());
      }
    } else { // Value into scalar
      if (value == null) {
        newValue = null;
      } else if (value instanceof String) {
        newValue = getConvertUtils().convert((String) value, type);
      } else if (value instanceof String[]) {
        newValue = getConvertUtils().convert(((String[]) value)[0], type);
      } else {
        newValue = convert(value, type);
      }
    }

    // Invoke the setter method
    try {
      if (index >= 0) {
        getPropertyUtils().setIndexedProperty(target, propName, index, newValue);
      } else if (key != null) {
        getPropertyUtils().setMappedProperty(target, propName, key, newValue);
      } else {
        getPropertyUtils().setProperty(target, propName, newValue);
      }
    } catch (NoSuchMethodException e) {
      throw new InvocationTargetException(e, "Cannot set " + propName);
    }

  }
}
